和多數程式語言一樣,在 Go 語言中,我們需要考慮如何設計方法。由於在 Go 語言中,方法本質上就是函數,所以我們之前講解的關於函數設計的內容同樣適用於方法,例如錯誤處理設計、針對異常的處理策略、使用 defer
提升簡潔性,等等。
Receiver(Receiver)也是設計方法時需要考量的一點。Receiver定義了一個Method所屬的類型,有點類似於其他語言中的 class 或 object 的概念,如下所示:
func (t T) M1() {}
func (t *T) M2() {}
上面範例中的 t
就是Receiver,而 M1
和 M2
是Receiver定義的Method。M1
方法代表Receiver類型為 T
的Method,而 M2
方法則代表Receiver類型為 *T
的Method。下面我們來看看不同的Receiver類型對 M1
和 M2
的影響。
T
時:代表 T
類型Object的Receiver參數以pass by value傳遞到 M1
方法中,實際上是 T
類型Object的副本,因此 M1
方法中對副本的任何修改操作,都不會影響原 T
類型Object。
*T
時:代表 *T
類型實例的Receiver參數以pass by reference傳遞到 M2
方法中,實際上是 T
類型Object的地址,因此 M2
方法可以通過該address對原 T
類型Object進行修改操作。
我們來看看一個更直觀的範例,來證明上述分析結果,並觀察 Go 方法選擇不同的Receiver類型對原類型Object的影響:
package main
type T struct {
a int
}
func (t T) M1() {
t.a = 10
}
func (t *T) M2() {
t.a = 11
}
func main() {
var t T
println(t.a) // 0
t.M1()
println(t.a) // 0
p := &t
p.M2()
println(t.a) // 11
}
在這個範例中,我們為類型 T
定義了兩個Method - M1
和 M2
,其中 M1
的Receiver類型為 T
,而 M2
的Receiver類型為 *T
。M1
和 M2
方法都通過Receiver參數 t
修改了 t
的attribute - a
。
了解了不同Receiver類型對 Go 方法的影響後,我們可以總結一下,日常寫程式中選擇Receiver參數類型時可以參考的原則:
*T
作為Receiver參數類型。
*T
作為Receiver參數類型,以避免複製大Object。
T
作為Receiver。
T
類型需要實作某個interface,那我們就要使用 T
作為Receiver參數類型,以滿足interface類型方法集合中的所有Method。如果 T
不需要實作某個interface,但 *T
需要實作該interface,則 *T
的方法集合包含 T
的方法集合。因此,我們在確定 Go 方法的Receiver類型時,可以參考上述原則。這裡可以解釋一下什麼是方法集合,我們先通過一個範例來直觀了解為什麼要有方法集合,以及它主要用來解決什麼問題:
type Interface interface {
M1()
M2()
}
type T struct{}
func (t T) M1() {}
func (t *T) M2() {}
func main() {
var t T
var pt *T
var i Interface
i = pt
i = t // cannot use t (type T) as type Interface in assignment: T does not implement Interface (M2 method has pointer receiver)
}
運行這個範例程式,我們在 i = t
這一行會得到 Go 編譯器的錯誤提示,Go 編譯器提示我們:T
沒有實作 Interface
類型方法列表中的 M2
,因此類型 T
的Object t
不能賦值給 Interface
變量。
Interface類型相對特殊,它只會列出代表interface的方法列表,不會具體定義某個方法,類似C++的virtual和Java的interface,其方法集合就是它的方法列表中的所有方法,我們可以一目了然地看到。因此,我們下面重點講解的是非interface類型的方法集合。
為了方便查看一個非interface類型的方法集合,我這裡提供了一個函數 dumpMethodSet
,用於輸出一個非interface類型的方法集合:
func dumpMethodSet(i interface{}) {
dynTyp := reflect.TypeOf(i)
if dynTyp == nil {
fmt.Printf("there is no dynamic type\n")
return
}
n := dynTyp.NumMethod()
if n == 0 {
fmt.Printf("%s's method set is empty!\n", dynTyp)
return
}
fmt.Printf("%s's method set:\n", dynTyp)
for j := 0; j < n; j++ {
fmt.Println("-", dynTyp.Method(j).Name)
}
fmt.Printf("\n")
}
下面我們利用這個函數,試著輸出一下 Go 原生類型以及自定義類型的方法集合:
type T struct{}
func (T) M1() {}
func (T) M2() {}
func (*T) M3() {}
func (*T) M4() {}
func main() {
var n int
dumpMethodSet(n)
dumpMethodSet(&n)
var t T
dumpMethodSet(t)
dumpMethodSet(&t)
}
我們得到如下結果:
int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2
*main.T's method set:
- M1
- M2
- M3
- M4
我們看到以 int
和 *int
為代表的 Go 原生類型,由於沒有定義Method,所以它們的方法集合都是空的。自定義類型 T
定義了方法 M1
和 M2
,因此其方法集合包含了 M1
和 M2
,也符合我們的預期。然而,*T
的方法集合中除了預期的 M3
和 M4
外,還包含了類型 T
的方法 M1
和 M2
!
這是因為,Go 語言規定,*T
類型的方法集合包含所有以 *T
為Receiver參數類型的方法,以及所有以 T
為Receiver參數類型的方法。這就是為何 *T
類型的方法集合包含四個方法的原因,以及第一個範例會報錯的原因。
T
時:方法Receiver是一個值,任何修改都不會影響原Object。*T
時:方法Receiver是一個pointer,可以修改原Object。*T
。*T
。T
。T
還是 *T
。這些原則可以幫助你在 Go 程式設計中做出更好的Method設計決策。